Skip to content

fix(collection): allow writes and reads to proceed during Optimize - #614

Merged
YongqiYin merged 12 commits into
alibaba:mainfrom
YongqiYin:fix/optimize-blocking-writes
Aug 5, 2026
Merged

fix(collection): allow writes and reads to proceed during Optimize#614
YongqiYin merged 12 commits into
alibaba:mainfrom
YongqiYin:fix/optimize-blocking-writes

Conversation

@YongqiYin

@YongqiYin YongqiYin commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

Summary

Optimize() held schema_handle_mtx_ exclusively for its whole duration, blocking Insert/Query/Fetch (shared mode) until the long compact finished. This PR makes the compact phase lock-free.

Changes

  • maintenance_mtx_ (new): serializes Optimize, schema DDLs, Close and Destroy before the schema lock. Waiters no longer become pending exclusive acquirers of the schema lock, so readers are never stalled by a queued maintenance operation.
  • Optimize in three phases: exclusive seal (writing segment dump + snapshot) → lock-free compact/index build → short exclusive commit (version apply/flush + segment publish + in-place index reload).
    MoveDirectory + Segment::Open run at the lock-free phase tail, so an open failure aborts before the manifest is persisted.
  • SegmentManager: internal shared_mutex (map was previously unsynchronized; reads and writes now race safely).

Tests

  • Concurrent regression UT: Optimize ‖ insert/fetch/query threads, asserts progress during compact and data correctness.
  • Superseded index file UT: exactly one index file per column after Optimize, and a fresh open reads all docs.
  • Full regression all pass.

Fixes #553

Optimize() held schema_handle_mtx_ exclusively for its whole duration,
so Insert/Query/Fetch/Delete (which take it in shared mode) were
blocked until the long-running compact finished.

- Downgrade Optimize's schema lock to shared mode; writes and reads now
  proceed during the compact and only wait on the short write_mtx_
  critical sections (flush and version commit). Schema operations and
  close/destroy still take the lock exclusively and remain mutually
  exclusive with Optimize.
- Add optimize_mtx_ to keep concurrent Optimize calls serialized,
  preserving the previous queuing semantics.
- Make SegmentManager internally thread-safe with a shared_mutex:
  readers (get_segments) can now run concurrently with the segment
  replacement in Optimize's commit phase.
- Add a regression test that asserts inserts and fetches make progress
  while a background Optimize is running.

Fixes alibaba#553
@YongqiYin
YongqiYin requested a review from zhourrr as a code owner July 23, 2026 09:22
Copilot AI lite review requested due to automatic review settings July 23, 2026 09:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

egolearner
egolearner previously approved these changes Jul 24, 2026

@egolearner egolearner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

基本ok,ut中验证optimize和写、fetch、query并发会更加完备

Copilot AI review requested due to automatic review settings July 24, 2026 09:20

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI review requested due to automatic review settings July 24, 2026 09:25
@YongqiYin
YongqiYin force-pushed the fix/optimize-blocking-writes branch from a478c5c to 63d26e0 Compare July 24, 2026 09:25

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Strengthening the UT to run Insert, Fetch and Query concurrently with a
background Optimize (review feedback) exposed a use-after-free: the
commit phase called reload_vector_index() on a live segment, destroying
vector indexers and mutating block metas that concurrent readers were
still using (readers only hold the shared schema lock).

- Commit the new segment set by reopening segments from their new metas
  and swapping instances, never mutating live segments in place;
  in-flight readers keep replaced instances alive via shared_ptr.
- Add SegmentManager::replace_segments() to apply the whole swap under
  one lock (validate first, then mutate), so readers never observe a
  partially committed segment set (no transient duplicate or missing
  segments), and drop the now-unused destroy_segment().
- Extend the regression test: writer, fetcher and querier threads now
  run concurrently with Optimize; worker failures are collected and
  asserted on the main thread. Query errors caused by the pre-existing
  Insert/Query visibility race in the writing segment (reproducible
  without Optimize) are tolerated and documented; a quiescent full-topk
  query is asserted after all threads join.
With Optimize committing via segment reopen-and-swap, the pre-index
vector files of a replaced segment instance (e.g. the flat file that a
newly built HNSW index supersedes) were no longer deleted and lingered
on disk until the segment was compacted away.

Reclaim them with the same mark-and-defer model segments already use:

- VectorColumnIndexer: add MarkDestroyOnRelease(); a marked indexer
  closes and removes its index file in the destructor, i.e. only after
  the last reference is released, so in-flight readers of the replaced
  segment instance keep the file alive until they finish.
- Segment: add mark_vector_index_files_for_removal(). Base and
  quantized indexers are marked independently: when a quantized index
  is built on top of a reused base file (see create_vector_index), only
  the old quantized file is superseded and the base file is still
  referenced by the new segment meta, so it must not be marked.
- Optimize: after the new segment set is committed, mark the replaced
  instances' superseded indexers.
- Add a regression test asserting exactly one index file remains per
  indexed column after Optimize and that a fresh open reads all docs.
Copilot AI review requested due to automatic review settings July 27, 2026 12:37

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@YongqiYin

Copy link
Copy Markdown
Collaborator Author
  • Commit 2 — 并发完备 UT + commit 阶段修复
    • 按建议把测试扩展为 insert / fetch / query 线程与后台 Optimize 并发运行,暴露出一处真实 use-after-free。
    • 根因:commit 阶段在 live segment 上调用 reload_vector_index(),销毁 indexer 并改写并发读者仍在使用的 block metas。
    • 修复:改为 reopen-and-swap 式提交——依据新 meta 打开新 segment 实例并原子交换(SegmentManager::replace_segments,单锁内 validate-then-mutate)。
    • 效果:读者只看到完整旧集合或完整新集合,不会看到 merge 输入与合并产物的混合态。
    • 在途读者经 shared_ptr 让被替换实例及其文件继续存活。
  • Commit 3 — 回收被取代的索引文件
    • swap 后,被新索引取代的 pre-index flat 文件会滞留磁盘,现改用 segment 既有的 mark-and-defer 模型移除。
    • 被标记的 indexer 在析构函数里删除自身文件,即最后一个引用释放后才删。
    • base 与 quantized indexer 独立标记:基于复用 base 文件构建的 quantized index(见 create_vector_index)只取代旧 quantized 文件,不动 base。
    • 关于 VectorColumnIndexer::Destroy():其 eager close-and-delete 已不在任何并发路径上——Optimize 走 swap + 延迟标记;其余调用方(CreateIndex / DropIndex 触发的 reload_vector_index)在排他 schema 锁下运行,读者全部被排除。

@YongqiYin
YongqiYin requested a review from egolearner July 29, 2026 02:39

@egolearner egolearner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

建议:使用 maintenance_mtx_ 简化 Optimize 并发方案

当前方案通过让 Optimize() 全程持有 schema_handle_mtx_ 的 shared lock,使 Query/Insert 可以在 compact 阶段继续执行。方向是对的,但它带来了一个新的问题:Optimize 提交时不能再原地调用 reload_vector_index(),因为并发 Query 可能仍在使用同一个 live Segment。

为解决这个问题,当前 PR 又引入了:

  • reopen 新 Segment 并按 ID 替换旧实例;
  • SegmentManager::replace_segments()
  • mark_vector_index_files_for_removal()
  • VectorColumnIndexer::MarkDestroyOnRelease()
  • 基于 shared_ptr 生命周期的延迟文件删除。

这套机制修改范围较大,Segment 发布和文件回收逻辑分散在 Collection、SegmentManager、Segment 和 VectorColumnIndexer 多层。另外,当前实现仍然在 write_mtx_ 内执行 Segment::Open(),并且 manifest 已经持久化后才 reopen;Open 失败时可能造成磁盘 version 与内存 SegmentManager 状态不一致。

这里可以考虑一个更小的方案:新增 maintenance_mtx_,将 Optimize 拆成“独占切换、锁外构建、独占提交”三个阶段。

1. maintenance_mtx_ 的职责

mutable std::mutex maintenance_mtx_;

这把锁只负责串行化会修改 schema、persisted segment 结构或者 collection 生命周期的维护操作:

  • Optimize
  • CreateIndex / DropIndex
  • AddColumn / AlterColumn / DropColumn
  • Close / Destroy
  • 另一个并发 Optimize

Query、Fetch、Insert、Update、Delete 不获取 maintenance_mtx_

Flush 可以有两种选择:

  • 为完全保持 main 的串行语义,也获取 maintenance_mtx_
  • 如果确认它只 flush 当前 writing segment,不切换 segment、不修改 version,则可以只保留 schema 独占锁,允许它在 Optimize compact 阶段执行。

所有维护操作必须遵守统一的锁顺序:

maintenance_mtx_
    -> schema_handle_mtx_
        -> write_mtx_
            -> SegmentManager::mutex_

尤其不能先拿 schema 独占锁再等待 maintenance_mtx_,否则一个等待 Optimize 的 CreateIndex 会长期占住 schema 锁,重新阻塞 Query/Insert。

2. Optimize 的三个阶段

Status CollectionImpl::Optimize(const OptimizeOptions &options) {
  CHECK_COLLECTION_READONLY_RETURN_STATUS;

  std::lock_guard maintenance_lock(maintenance_mtx_);

  std::vector<Segment::Ptr> persist_segments;

  // Phase 1: safely seal the current writing segment.
  {
    std::unique_lock schema_lock(schema_handle_mtx_);
    std::lock_guard write_lock(write_mtx_);

    CHECK_DESTROY_RETURN_STATUS(destroyed_, false);

    if (writing_segment_->has_record()) {
      auto s = switch_to_new_segment_for_writing();
      CHECK_RETURN_STATUS(s);
    }

    persist_segments = get_all_persist_segments();
  }

  if (persist_segments.empty()) {
    return Status::OK();
  }

  // Phase 2: expensive compact/index build without schema/write lock.
  auto delete_store_clone = delete_store_->clone();
  auto tasks =
      build_compact_task(schema_, persist_segments, options.concurrency_,
                         delete_store_clone->make_filter());

  auto s = execute_compact_task(tasks);
  CHECK_RETURN_STATUS(s);

  // Phase 3: short exclusive commit.
  {
    std::unique_lock schema_lock(schema_handle_mtx_);
    std::lock_guard write_lock(write_mtx_);

    s = commit_optimize_tasks(tasks);
    CHECK_RETURN_STATUS(s);
  }

  return Status::OK();
}

Phase 2 不需要持有 schema_handle_mtx_

  • maintenance_mtx_ 保证 schema DDL、Close、Destroy 不会在 compact 期间执行;
  • Optimize 持有输入 Segment 的 shared_ptr
  • Insert 只能增加新的 persisted segment,不会修改 Optimize 已经捕获的 persisted segment;
  • commit 阶段重新读取最新 Version,并只移除本次 compact 的输入 Segment,因此 Optimize 期间新产生的 Segment 会被保留。

3. 为什么可以恢复 reload_vector_index()

Phase 3 获取 schema_handle_mtx_ 独占锁后:

  • 已经运行的 Query/Fetch 必须先结束并释放 shared lock;
  • 新 Query/Fetch 无法进入;
  • Insert/Delete 同样无法进入;
  • 没有 reader 正在使用旧 Segment 的 vector indexer。

因此可以安全复用 main 原来的提交逻辑:

create_index_task.input_segment_->reload_vector_index(
    *schema_, create_index_task.output_segment_meta_,
    create_index_task.output_vector_indexers_,
    create_index_task.output_quant_vector_indexers_);

这样可以删除:

  • 同 ID Segment reopen-and-swap;
  • SegmentManager::replace_segments() 的特殊替换语义;
  • mark_vector_index_files_for_removal()
  • VectorColumnIndexer::MarkDestroyOnRelease()
  • indexer 析构时的延迟文件删除。

旧索引文件可以在独占 commit 阶段立即关闭和删除,因为所有旧 reader 已经退出。

4. CreateIndex 的修改示例

CreateIndex 本身不需要拆阶段,只需在 schema 锁之前获取 maintenance lock:

Status CollectionImpl::CreateIndex(
    const std::string &column_name,
    const IndexParams::Ptr &index_params,
    const CreateIndexOptions &options) {
  CHECK_COLLECTION_READONLY_RETURN_STATUS;

  std::lock_guard maintenance_lock(maintenance_mtx_);
  std::unique_lock schema_lock(schema_handle_mtx_);

  CHECK_DESTROY_RETURN_STATUS(destroyed_, false);

  // Existing CreateIndex implementation remains unchanged.
  ...
}

假设 Optimize 正在 compact:

  1. Optimize 持有 maintenance_mtx_,但 Phase 2 不持 schema 锁;
  2. CreateIndex 阻塞在 maintenance_mtx_,不会提前占用 schema 独占锁;
  3. Query/Insert 仍然可以正常获得 schema shared lock;
  4. Optimize 进入 Phase 3,短暂获取 schema 独占锁完成提交;
  5. Optimize 释放 maintenance_mtx_ 后,CreateIndex 才开始执行。

这样可以保证 CreateIndex 不会基于一份正在被 Optimize 重写的 Segment set 修改 schema/index,也不会因为等待 Optimize 而间接阻塞正常 Query/Insert。

5. SegmentManager 仍需保留 shared mutex

SegmentManager 的内部 shared_mutex 仍然需要保留。

Optimize Phase 2 期间:

  • Query 可能调用 get_segments()
  • Insert 可能切换 writing segment 并调用 add_segment()

因此 map 的读写仍需分别使用 shared/unique lock。但 commit 阶段已经持有 schema 独占锁,不再需要复杂的同 ID原子 replacement,可以恢复简单的 add_segment() / destroy_segment()

6. 并发语义

采用该方案后:

  • Optimize 与 DDL、Close、Destroy:仍然串行;
  • 两个 Optimize:仍然串行;
  • Optimize Phase 1/3 与 Query/Insert:短暂串行;
  • Optimize Phase 2 与 Query/Fetch/Insert/Delete:可以并发;
  • CreateIndex 与 Query/Insert:行为保持 main 不变,仍然串行;
  • CreateIndex 等待 Optimize 时不会持有 schema 锁,因此不会阻塞 Query/Insert。

该方案只放开 issue 要求的“Optimize 耗时 compact/rebuild 阶段与普通读写并发”,不扩展到并发 DDL。

7. 已知边界

这个方案不解决两个已有问题:

  1. switch_to_new_segment_for_writing() 仍在 Phase 1 执行 dump(),如果 dump 较慢,第一次停顿仍可能偏长。彻底解决需要 sealed segment,将 writing segment 切换和 dump 解耦。
  2. VersionManager::apply() + flush() 的失败原子性仍然不够完整,可以后续独立改成 durable commit(version)

整体上,这个方案保留 main 已有的独占提交模型,只将耗时 compact 阶段移出 schema/write 锁,修改范围更小,并且不需要引入新的 Segment 和索引文件生命周期协议。

…view feedback

Per review feedback on the shared-lock model:
a maintenance operation waiting for a running Optimize became a
pending exclusive acquirer of the schema lock, blocking new readers
on typical shared_mutex implementations.

Changes:
- Replace optimize_mtx_ with maintenance_mtx_ (acquired by Optimize,
  schema DDLs, Flush, Close and Destroy before the schema lock).
  Lock order: maintenance -> schema -> write -> SegmentManager.
- Restructure Optimize into three phases: exclusive seal, lock-free
  compact (MoveDirectory + Segment::Open moved to lock-free phase 2
  tail, so an open failure aborts before the manifest is persisted),
  and short exclusive commit that restores in-place reload_vector_index
  with eager file removal (no reader can hold indexers under exclusive
  schema lock).
- Downgrade read-only Stats()/Schema()/Options() to shared locking.
- Remove replace_segments() and the deferred index-file removal
  machinery introduced earlier in this PR; they are only needed by
  lock-free consumers (DocIterator, alibaba#597) and will land there.
- Keep and adapt the two regression UTs (concurrent read-write and
  superseded index file count).
Copilot AI review requested due to automatic review settings August 4, 2026 03:22

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (4)

tests/db/collection_test.cc:3039

  • The test currently ignores all Query() errors, which can mask real regressions introduced by the new Optimize concurrency (e.g., schema/version/segment visibility issues). To keep the regression signal strong, only tolerate the specific expected transient failure (ideally by checking status code/type or a known message substring) and record any other error via record_error().
      auto result = collection->Query(query);
      if (!result.has_value()) {
        // Tolerated: a doc admitted by the writing segment's streaming
        // vector index may not yet be visible in its forward store,
        // transiently failing the query. Pre-existing Insert/Query race,
        // unrelated to Optimize.
        continue;
      }

tests/db/collection_test.cc:2944

  • Using a fixed sleep_for(200ms) to 'ensure Optimize is running' is prone to flakiness across different machines and build types (Optimize could finish before workers start, or not reach the intended phase). A more deterministic approach would be to add an explicit synchronization point (e.g., a test hook/failpoint or a latch signaled when Optimize enters the compact phase) so the worker threads reliably exercise the intended concurrency window.
  // give the optimizer time to take its locks and start compacting
  std::this_thread::sleep_for(std::chrono::milliseconds(200));

src/db/index/segment/segment_manager.cc:171

  • segment_id is not used in this loop. If the project builds with warnings-as-errors, this structured binding can trigger an unused-variable warning on some toolchains. Consider iterating as for (auto &pair : segments_map_) and pushing pair.second, or otherwise avoid binding the unused key.
    for (auto &[segment_id, segment] : segments_map_) {
      segments.push_back(segment);
    }

tests/db/collection_test.cc:3031

  • This uses a C-style cast to convert raw float bytes into a std::string. Prefer reinterpret_cast<const char*> here to avoid dropping constness and to make the intent explicit; it also helps prevent compiler warnings in stricter builds.
      query.target_.set_vector(
          std::string((char *)vector.value().data(),
                      vector.value().size() * sizeof(float)));

Comment thread tests/db/collection_test.cc Outdated
Copilot AI review requested due to automatic review settings August 4, 2026 06:07

This comment was marked as duplicate.

Restore Stats()/Schema()/Options() to std::lock_guard.
…ilot review

- Tolerate only the known transient Query failure (error message
  containing 'fetch table failed', a pre-existing writing-segment
  Insert/Query visibility race); any other error is now recorded as a
  failure instead of being silently ignored.
- Replace C-style casts with reinterpret_cast<const char*> in the new
  test code (const-correctness).
- Drop the unused structured binding in SegmentManager::drop_column
  (avoids warnings under warnings-as-errors).
Copilot AI review requested due to automatic review settings August 4, 2026 06:31

This comment was marked as duplicate.

…ncy UT

Per Copilot review:
- Only spot-check docs beyond initial_doc_count when the writer
  actually made progress during Optimize; a no-insert run (e.g. a
  very fast Optimize) previously caused a false failure fetching a
  doc that was never inserted.
- Yield in the fetch/query worker loops to reduce CPU load under CI
  contention without affecting progress measurement.
Copilot AI review requested due to automatic review settings August 4, 2026 06:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.

Suppressed comments (4)

tests/db/collection_test.cc:2944

  • A fixed 200ms sleep makes the test timing-dependent and potentially flaky on slow/fast machines (Optimize may not have started yet, or may have already finished). Prefer a deterministic synchronization point (e.g., a hook/latch indicating compaction has entered phase 2), or at least poll with a bounded wait for an observable state that implies Optimize is running before starting progress assertions.
  // give the optimizer time to take its locks and start compacting
  std::this_thread::sleep_for(std::chrono::milliseconds(200));

src/db/collection.cc:866

  • If Segment::Open() fails after MoveDirectory() succeeds, Optimize() returns early leaving an orphaned segment directory at its final path that is not referenced by the manifest/version. Consider adding rollback cleanup (e.g., delete/move-back new_segment_path) on failure paths after the move, and similarly consider cleanup if the later version apply/flush fails.
    if (!FileHelper::MoveDirectory(tmp_segment_path, new_segment_path)) {
      return Status::InternalError("move segment directory failed");
    }
    compact_task.output_segment_meta_->set_id(new_segment_id);

    auto new_segment =
        Segment::Open(path_, *schema_, *compact_task.output_segment_meta_,
                      id_map_, delete_store_, version_manager_,
                      SegmentOptions{true, options_.enable_mmap_});
    if (!new_segment.has_value()) {
      return new_segment.error();
    }

src/db/index/segment/segment_manager.cc:31

  • std::unique_lock is defined in <mutex>. Relying on indirect includes via <shared_mutex> is not guaranteed to be portable across standard library implementations. Ensure <mutex> is included in the relevant header/translation unit where std::unique_lock / std::shared_lock are used.
  std::unique_lock<std::shared_mutex> lock(mutex_);

src/db/collection.cc:247

  • The PR description mentions adding an optimize_mtx_ to serialize concurrent Optimize() calls, but the implementation introduces maintenance_mtx_ that serializes a broader set of operations (Optimize + DDL + Flush + Close/Destroy). Either update the PR description to match the new behavior/lock, or adjust the code to align with the described optimize_mtx_ scope if the broader serialization is unintended.
  // Serializes maintenance operations (Optimize, schema DDL, Flush, Close
  // and Destroy) without holding schema_handle_mtx_, so a maintenance
  // operation waiting for a running Optimize never becomes a pending
  // exclusive acquirer of the schema lock (which would block new readers).
  // Lock order: maintenance -> schema -> write -> SegmentManager; never
  // acquire maintenance_mtx_ after any of the others.
  mutable std::mutex maintenance_mtx_;

Comment thread tests/db/collection_test.cc
Comment thread src/db/collection.cc
Comment thread src/db/collection.cc Outdated
…currency UT

Per review feedback:

- Flush() only flushes the writing segment's WAL (no schema change, no segment switch, no version update), so it no longer takes maintenance_mtx_
- Guard opened_segments[opened_index++] with a bounds check in the Optimize commit phase.
- Cap the fetcher/querier worker loops at 100k iterations so a hung Optimize cannot hang the test forever.
- Assert Stats() succeeded before reading its value in the concurrent regression test.
Copilot AI review requested due to automatic review settings August 4, 2026 08:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

tests/db/collection_test.cc:2944

  • This fixed sleep makes the concurrency regression test timing-dependent and potentially flaky (Optimize may still be in phase 1 or may already complete on fast/slow machines). Prefer a deterministic sync point (e.g., a barrier/latch exposed via test hook/observer), or at minimum assert Optimize is still running after the delay and GTEST_SKIP() (or adjust workload) if it already finished.
  // let the optimizer reach the compact phase
  std::this_thread::sleep_for(std::chrono::milliseconds(200));

tests/db/collection_test.cc:3068

  • The way ops_during_optimize is counted (if (!optimize_done.load()) after each operation) can undercount operations that overlapped with Optimize but finished right as optimize_done flips, causing intermittent failures. Consider counting using a pre/post sample (e.g., read a local was_optimizing = !optimize_done.load() before the op) or use a separate 'entered_compact_phase' signal to measure progress during the intended window.
  // if any had been blocked for the whole Optimize, no ops could have
  // completed while Optimize was still running
  ASSERT_GE(writer_result.ops_during_optimize, 2);
  ASSERT_GE(fetch_result.ops_during_optimize, 2);
  ASSERT_GE(query_result.ops_during_optimize, 2);

tests/db/collection_test.cc:3123

  • This directly calls .value() on the Stats() result without checking has_value(), which will crash the test on error instead of producing an assertion failure with context. Add ASSERT_TRUE(collection->Stats().has_value()) (or store the result and assert) before accessing .value().
  auto stats = collection->Stats().value();

src/db/index/segment/segment_manager.cc:58

  • Holding the SegmentManager exclusive lock while calling segment->destroy() can block all concurrent segment readers for the duration of potentially slow I/O, and increases the risk of lock coupling issues if destroy() calls back into code that needs SegmentManager. A safer pattern is to remove (or move-out) the Segment::Ptr under lock, release the lock, then perform destroy() without holding mutex_.
  std::unique_lock<std::shared_mutex> lock(mutex_);
  auto iter = segments_map_.find(segment_id);
  if (iter == segments_map_.end()) {
    return Status::NotFound("Segment not found");
  }

  auto s = iter->second->destroy();
  CHECK_RETURN_STATUS(s);

  segments_map_.erase(iter);
  return Status::OK();

src/db/collection.cc:864

  • If MoveDirectory(...) succeeds but Segment::Open(...) fails, Optimize returns early leaving an orphaned moved segment directory on disk that is not referenced by the manifest/version. Consider adding best-effort cleanup on failure (e.g., delete the moved directory or move it back / move it to a quarantine tmp path) to prevent disk leaks and confusing leftovers.
    if (!FileHelper::MoveDirectory(tmp_segment_path, new_segment_path)) {
      return Status::InternalError("move segment directory failed");
    }
    compact_task.output_segment_meta_->set_id(new_segment_id);

    auto new_segment =
        Segment::Open(path_, *schema_, *compact_task.output_segment_meta_,
                      id_map_, delete_store_, version_manager_,
                      SegmentOptions{true, options_.enable_mmap_});
    if (!new_segment.has_value()) {
      return new_segment.error();
    }

Comment thread tests/db/collection_test.cc Outdated
… layout

Per Copilot review:

- Count worker ops by sampling optimize_done before the operation, so
  ops that overlap the Optimize completion boundary are not lost.
- Walk all segment directories recursively when counting superseded
  index files, instead of assuming the persisted segment lives in /0.
- Assert Stats() has_value() in the superseded-file test.
- Remove the moved-but-unopened segment directory on Segment::Open
  failure (best-effort cleanup, manifest not yet persisted).
Copilot AI review requested due to automatic review settings August 4, 2026 10:46

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

Suppressed comments (2)

src/db/index/segment/segment_manager.cc:144

  • The async lambda in this loop uses [&] capture, but it indirectly captures the loop-local segment reference. Because that reference is reassigned each iteration, async tasks can end up operating on the wrong segment. Capture the Segment::Ptr by value in the lambda.
  for (size_t i = 0; i < segments.size(); i += concurrency) {
    size_t end = std::min(i + concurrency, segments.size());

src/db/collection.cc:867

  • If MoveDirectory() or Segment::Open() fails, only the current new_segment_path is cleaned up. Any segments already moved/opened earlier in this loop will be left orphaned on disk (not referenced by any manifest), which can accumulate garbage in the collection directory. Consider best-effort cleanup of all previously moved segments on these early-return paths.
    if (!FileHelper::MoveDirectory(tmp_segment_path, new_segment_path)) {
      return Status::InternalError("move segment directory failed");
    }
    compact_task.output_segment_meta_->set_id(new_segment_id);

Comment thread src/db/index/segment/segment_manager.cc
…n up all moved dirs

Per Copilot review:

- In SegmentManager::add_column/alter_column, the std::async lambda
  captured the loop-local 'segment' reference by reference; since it
  is rebinding each iteration, async tasks could operate on the wrong
  segment. Capture it by value instead (pre-existing in main, fixed
  while touching these functions).
- In Optimize's lock-free rename/open loop, track all moved segment
  directories and remove them (best-effort) on any failure, so earlier
  successfully-moved dirs do not accumulate as orphans.
Copilot AI review requested due to automatic review settings August 4, 2026 11:54

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/db/index/segment/segment_manager.cc:33

  • std::unique_lock<std::shared_mutex> is used in this file, but the translation unit doesn't include <mutex> (it currently only includes <algorithm>, <future>, <thread>, etc.). Relying on transitive includes is fragile and can break builds on some standard library implementations; include <mutex> explicitly.
Status SegmentManager::add_segment(Segment::Ptr segment) {
  if (!segment) {
    return Status::InvalidArgument("Segment is null");
  }

  std::unique_lock<std::shared_mutex> lock(mutex_);
  segments_map_[segment->id()] = segment;
  return Status::OK();

@egolearner egolearner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@YongqiYin
YongqiYin merged commit 16c14dc into alibaba:main Aug 5, 2026
24 of 25 checks passed
YongqiYin added a commit to YongqiYin/zvec that referenced this pull request Aug 5, 2026
Extend the reviewer's shared_ptr-reduction feedback to the remaining
hot paths:
- ConvertArrowRowToDocField now takes a raw Array* (callers pass
  batch.columns()[i].get() / chunk.get()), removing per-row atomic
  ref-count traffic.
- DocIterator PK/doc_id/row-id extraction uses type_id() + static_cast
  instead of per-row dynamic_pointer_cast.
- Update the Scan comment after alibaba#614: snapshot consistency comes from
  the write_mtx_ atomic snapshot + shared_ptr keep-alive, not from
  blocking Optimize (which now takes the schema lock in shared mode).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: optimize会阻塞insert吗?

3 participants